/**
 * Copyright 2008 Mathias Doenitz, http://lis.to/
 *
 * This file is part of the lis.to java desktop client. The lis.to java desktop client is free software: you can
 * redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option) any later version.
 *
 * The lis.to java desktop client is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along with the lis.to java desktop client.
 * If not, see http://www.gnu.org/licenses/
 */

package listo.client.model;

import com.google.common.collect.Lists;
import com.thoughtworks.xstream.annotations.XStreamAlias;
import listo.utils.types.Copyable;
import org.apache.commons.lang.StringUtils;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * A context describes and manages a consistent state of all user objects (task and folders).
 */
@XStreamAlias("context")
public class Context implements Copyable<Context> {

    // the root of the folder hierarchy
    @XStreamAlias("folders")
    private Folder folderRoot = new Folder();

    // the list of all tasks in the context
    @XStreamAlias("tasks")
    private Tasks tasks = new Tasks();

    // an index Id -> Folder of all folders in the context EXCEPT the root folder
    private transient Map<ObjectId, Folder> folderIndex = new HashMap<ObjectId, Folder>();

    // an index Id -> Task
    private transient Map<ObjectId, Task> taskIndex = new HashMap<ObjectId, Task>();

    /**
     * @return the root folder
     */
    public Folder getRootFolder() {
        return folderRoot;
    }

    /**
     * Returns the parent folder with the given id. If the id is null the method returns the root folder.
     *
     * @param parentId the parent folder id
     * @return the parent folder
     */
    public Folder getParentFolder(ObjectId parentId) {
        return parentId != null ? getFolder(parentId) : folderRoot;
    }

    /**
     * @return all tasks in the context
     */
    public Tasks getTasks() {
        return tasks;
    }

    /**
     * @return all folders in the context in a flat list
     */
    public List<Folder> getAllFolders() {
        return Lists.newArrayList(folderIndex.values());
    }

    /**
     * Gets the folders with the given id.
     *
     * @param id the id
     * @return the folder
     */
    public Folder getFolder(ObjectId id) {
        Folder folder = folderIndex.get(id);
        if (folder == null) {
            throw new IllegalOperationException("Tried to retrieve a non-exisiting folder (id %1$s)", id);
        }
        return folder;
    }

    /**
     * Gets the task with the given id.
     *
     * @param id the id
     * @return the task
     */
    public Task getTask(ObjectId id) {
        Task task = taskIndex.get(id);
        if (task == null) {
            throw new IllegalOperationException("Tried to retrieve a non-exisiting task (id %1$s)", id);
        }
        return task;
    }

    /**
     * Gets the folder with the given path name or null.
     *
     * @param pathName the path name
     * @return the folder
     */
    public Folder getFolderByPathName(String pathName) {
        if (StringUtils.isEmpty(pathName)) return null;
        String[] split = StringUtils.split(pathName, '/');
        return getFolderByPathName(folderRoot, split, 0);
    }

    // private helper
    private Folder getFolderByPathName(Folder parent, String[] segments, int level) {
        if (level < segments.length) {
            String name = segments[level];
            if (StringUtils.isNotEmpty(name) && parent.hasFolders()) {
                for (Folder subFolder : parent.getFolders()) {
                    if (name.equals(subFolder.getName())) {
                        return level < segments.length - 1 ?
                                getFolderByPathName(subFolder, segments, level + 1) : subFolder;
                    }
                }
            }
        }
        return null;
    }

    /**
     * Puts the given folder into the index.
     *
     * @param folder the folder
     */
    public void indexFolder(Folder folder) {
        folderIndex.put(folder.getId(), folder);
    }

    /**
     * Returns the index of this task in the task list.
     *
     * @param task the task
     * @return the index or -1
     */
    public int indexOf(Task task) {
        return tasks.indexOf(task);
    }

    /**
     * Adds the given task to the context.
     *
     * @param task the task
     */
    public void add(Task task) {
        if (taskIndex.containsKey(task.getId())) {
            throw new IllegalOperationException("Tried to add an already existing task (id %1$s)", task.getId());
        }
        tasks.add(task);
        indexTask(task);
    }

    /**
     * Adds the given task to the context.
     *
     * @param task     the task
     * @param position the position at which to add the new task
     */
    public void add(Task task, int position) {
        if (taskIndex.containsKey(task.getId())) {
            throw new IllegalOperationException("Tried to add an already existing task (id %1$s)", task.getId());
        }
        tasks.add(position, task);
        indexTask(task);
    }

    /**
     * Puts the given task into the index.
     *
     * @param task the task
     */
    public void indexTask(Task task) {
        taskIndex.put(task.getId(), task);
    }

    /**
     * Deletes the task with the given id from the context.
     *
     * @param id the task id
     * @return the task that was deleted
     */
    public Task deleteTask(ObjectId id) {
        Task task = taskIndex.remove(id);
        if (task == null) {
            throw new IllegalOperationException("Tried to delete a non-existing task (id %1$s)", id);
        }
        tasks.remove(task);
        return task;
    }

    /**
     * Deletes the folder with the given id from the context.
     *
     * @param id the folder id
     */
    public void deleteFolder(ObjectId id) {
        Folder folder = getFolder(id);

        // first recursively delete all sub folders
        if (folder.hasFolders()) {
            for (Folder subFolder : new ArrayList<Folder>(folder.getFolders())) {
                deleteFolder(subFolder.getId());
            }
        }

        Folder parent = folder.getParent();
        parent.delete(folder);
        folderIndex.remove(id);

        for (Task task : tasks) {
            if (task.hasFolders(id)) task.detachFolder(id);
        }
    }

    /**
     * Gets the total number of folders currently in the context.
     *
     * @return the number
     */
    public int getFoldersCount() {
        return folderIndex.size();
    }

    /**
     * Gets the total number of tasks currently in the context.
     *
     * @return the number
     */
    public int getTaskCount() {
        return tasks.size();
    }

    /**
     * Special method called after xstream deserialization to perform inititialization of transient fields.
     *
     * @return this
     */
    private Object readResolve() {
        folderIndex = new HashMap<ObjectId, Folder>();
        indexSubTree(folderRoot);
        taskIndex = new HashMap<ObjectId, Task>(tasks.size());
        for (Task task : tasks) indexTask(task);
        return this;
    }

    // private helper
    private void indexSubTree(Folder folder) {
        if (folder.hasFolders()) {
            for (Folder subFolder : folder.getFolders()) {
                indexFolder(subFolder);
                indexSubTree(subFolder);
            }
        }
    }

    /**
     * Returns a deep copy of this context, i.e. a completely independent object tree representing the same
     * user state.
     *
     * @return the copied context
     */
    public Context copy() {
        Context clone = new Context();
        clone.folderRoot = folderRoot.copy();
        clone.tasks = tasks.copy();
        clone.readResolve();
        return clone;
    }

}
